// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package gatewayapi

import (
	"context"
	"fmt"
	"strings"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/stretchr/testify/assert"
	k8stypes "k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"

	"github.com/apache/apisix-ingress-controller/test/e2e/framework"
	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
)

const _secretName = "test-apisix-tls"

var Cert = strings.TrimSpace(framework.TestServerCert)

var Key = strings.TrimSpace(framework.TestServerKey)

func createSecret(s *scaffold.Scaffold, secretName string) {
	err := s.NewKubeTlsSecret(secretName, Cert, Key)
	assert.Nil(GinkgoT(), err, "create secret error")
}

var _ = Describe("Test Gateway", Label("networking.k8s.io", "gateway"), func() {
	s := scaffold.NewDefaultScaffold()

	var gatewayProxyYaml = `
apiVersion: apisix.apache.org/v1alpha1
kind: GatewayProxy
metadata:
  name: apisix-proxy-config
  namespace: %s
spec:
  provider:
    type: ControlPlane
    controlPlane:
      endpoints:
      - %s
      auth:
        type: AdminKey
        adminKey:
          value: "%s"
`

	Context("Gateway", func() {
		var defaultGatewayClass = `
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: %s
spec:
  controllerName: "%s"
`

		var defaultGateway = `
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: %s
spec:
  gatewayClassName: %s
  listeners:
    - name: http1
      protocol: HTTP
      port: 80
  infrastructure:
    parametersRef:
      group: apisix.apache.org
      kind: GatewayProxy
      name: apisix-proxy-config
`

		var noClassGateway = `
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: apisix-not-class
spec:
  gatewayClassName: apisix-not-exist
  listeners:
    - name: http1
      protocol: HTTP
      port: 80
  infrastructure:
    parametersRef:
      group: apisix.apache.org
      kind: GatewayProxy
      name: apisix-proxy-config
`

		It("Create Gateway", func() {
			gatewayClassName := s.Namespace()
			By("create GatewayProxy")
			gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Namespace(), s.Deployer.GetAdminEndpoint(), s.AdminKey())
			err := s.CreateResourceFromString(gatewayProxy)
			Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
			time.Sleep(5 * time.Second)

			By("create GatewayClass")
			err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(defaultGatewayClass, gatewayClassName, s.GetControllerName()), "")
			Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
			time.Sleep(5 * time.Second)

			By("check GatewayClass condition")
			gcyaml, err := s.GetResourceYaml("GatewayClass", gatewayClassName)
			Expect(err).NotTo(HaveOccurred(), "getting GatewayClass yaml")
			Expect(gcyaml).To(ContainSubstring(`status: "True"`), "checking GatewayClass condition status")
			Expect(gcyaml).To(ContainSubstring("message: the gatewayclass has been accepted by the apisix-ingress-controller"), "checking GatewayClass condition message")

			By("create Gateway")
			gatewayName := s.Namespace()
			err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(defaultGateway, gatewayName, gatewayClassName), s.
				Namespace())
			Expect(err).NotTo(HaveOccurred(), "creating Gateway")
			time.Sleep(5 * time.Second)

			By("check Gateway condition")
			gwyaml, err := s.GetResourceYaml("Gateway", gatewayName)
			Expect(err).NotTo(HaveOccurred(), "getting Gateway yaml")
			Expect(gwyaml).To(ContainSubstring(`status: "True"`), "checking Gateway condition status")
			Expect(gwyaml).To(ContainSubstring("message: the gateway has been accepted by the apisix-ingress-controller"), "checking Gateway condition message")

			By("create Gateway with not accepted GatewayClass")
			err = s.CreateResourceFromStringWithNamespace(noClassGateway, s.Namespace())
			Expect(err).NotTo(HaveOccurred(), "creating Gateway")
			time.Sleep(5 * time.Second)

			By("check Gateway condition")
			gwyaml, err = s.GetResourceYaml("Gateway", "apisix-not-class")
			Expect(err).NotTo(HaveOccurred(), "getting Gateway yaml")
			Expect(gwyaml).To(ContainSubstring(`status: Unknown`), "checking Gateway condition status")
		})
	})

	Context("Gateway SSL", func() {
		It("Check if SSL resource was created", func() {
			By("create GatewayProxy")
			gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Namespace(), s.Deployer.GetAdminEndpoint(), s.AdminKey())
			err := s.CreateResourceFromString(gatewayProxy)
			Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
			time.Sleep(5 * time.Second)

			By("create secret")
			secretName := _secretName
			host := "api6.com"
			createSecret(s, secretName)
			gatewayClassName := s.Namespace()
			var defaultGatewayClass = `
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: %s
spec:
  controllerName: "%s"
`

			var defaultGateway = fmt.Sprintf(`
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: %s
spec:
  gatewayClassName: %s
  listeners:
    - name: http1
      protocol: HTTPS
      port: 443
      hostname: %s
      tls:
        certificateRefs:
        - kind: Secret
          group: ""
          name: %s
  infrastructure:
    parametersRef:
      group: apisix.apache.org
      kind: GatewayProxy
      name: apisix-proxy-config
`, s.Namespace(), gatewayClassName, host, secretName)
			By("create GatewayClass")
			err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(defaultGatewayClass, gatewayClassName, s.GetControllerName()), "")
			Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
			time.Sleep(5 * time.Second)

			By("create Gateway")
			err = s.CreateResourceFromStringWithNamespace(defaultGateway, s.Namespace())
			Expect(err).NotTo(HaveOccurred(), "creating Gateway")
			time.Sleep(10 * time.Second)

			tls, err := s.DefaultDataplaneResource().SSL().List(context.Background())
			assert.Nil(GinkgoT(), err, "list tls error")
			assert.Len(GinkgoT(), tls, 1, "tls number not expect")
			assert.Len(GinkgoT(), tls[0].Certificates, 1, "length of certificates not expect")
			assert.Equal(GinkgoT(), Cert, tls[0].Certificates[0].Certificate, "tls cert not expect")
			assert.ElementsMatch(GinkgoT(), []string{host}, tls[0].Snis)
		})

		It("Gateway SSL with and without hostname", func() {
			By("create GatewayProxy")
			gatewayProxy := fmt.Sprintf(gatewayProxyYaml, s.Namespace(), s.Deployer.GetAdminEndpoint(), s.AdminKey())
			err := s.CreateResourceFromString(gatewayProxy)
			Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
			time.Sleep(5 * time.Second)

			secretName := _secretName
			createSecret(s, secretName)
			gatewayClassName := s.Namespace()
			var defaultGatewayClass = `
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: %s
spec:
  controllerName: "%s"
`

			var defaultGateway = fmt.Sprintf(`
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: same-namespace-with-https-listener
spec:
  gatewayClassName: %s
  listeners:
  - name: https
    port: 443
    protocol: HTTPS
    allowedRoutes:
      namespaces:
        from: Same
    tls:
      certificateRefs:
      - group: ""
        kind: Secret
        name: %s
  - name: https-with-hostname
    port: 443
    hostname: api6.com
    protocol: HTTPS
    allowedRoutes:
      namespaces:
        from: Same
    tls:
      certificateRefs:
      - group: ""
        kind: Secret
        name: %s
  infrastructure:
    parametersRef:
      group: apisix.apache.org
      kind: GatewayProxy
      name: apisix-proxy-config
`, gatewayClassName, secretName, secretName)
			By("create GatewayClass")
			err = s.CreateResourceFromStringWithNamespace(fmt.Sprintf(defaultGatewayClass, gatewayClassName, s.GetControllerName()), "")
			Expect(err).NotTo(HaveOccurred(), "creating GatewayClass")
			time.Sleep(5 * time.Second)

			By("create Gateway")
			err = s.CreateResourceFromStringWithNamespace(defaultGateway, s.Namespace())
			Expect(err).NotTo(HaveOccurred(), "creating Gateway")

			Eventually(func() error {
				tls, err := s.DefaultDataplaneResource().SSL().List(context.Background())
				Expect(err).NotTo(HaveOccurred(), "list ssl")
				if len(tls) != 2 {
					return fmt.Errorf("expect 2 ssl, got %d", len(tls))
				}
				if len(tls[0].Certificates) != 1 {
					return fmt.Errorf("expect 1 certificate, got %d", len(tls[0].Certificates))
				}
				if Cert != tls[0].Certificates[0].Certificate {
					return fmt.Errorf("expect cert %s, got %s", Cert, tls[0].Certificates[0].Certificate)
				}
				if s.GetControllerName() != tls[0].Labels["k8s/controller-name"] {
					return fmt.Errorf("expect controller name %s, got %s", s.GetControllerName(), tls[0].Labels["k8s/controller-name"])
				}
				return nil
			}).WithTimeout(20 * time.Second).ProbeEvery(time.Second).ShouldNot(HaveOccurred())

			By("update secret")
			err = s.NewKubeTlsSecret(secretName, framework.TestCert, framework.TestKey)
			Expect(err).NotTo(HaveOccurred(), "update secret")
			Eventually(func() string {
				tls, err := s.DefaultDataplaneResource().SSL().List(context.Background())
				Expect(err).NotTo(HaveOccurred(), "list ssl")
				if len(tls) != 2 {
					return ""
				}
				if len(tls[0].Certificates) < 1 {
					return ""
				}
				return tls[0].Certificates[0].Certificate
			}).WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(framework.TestCert))
		})
	})

	Context("Gateway Status", func() {
		var gatewaySpec = `
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: %s
spec:
  gatewayClassName: %s
  listeners:
  - name: http
    protocol: HTTP
    port: 80
  - name: tcp
    protocol: TCP
    port: 9000
    allowedRoutes:
      kinds:
      - kind: TCPRoute
  - name: udp
    protocol: UDP
    port: 80
    allowedRoutes:
      kinds:
      - kind: UDPRoute
  infrastructure:
    parametersRef:
      group: apisix.apache.org
      kind: GatewayProxy
      name: apisix-proxy-config
`
		var httprouteSpec = `
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: %s
spec:
  parentRefs:
  - name: %s
  hostnames:
  - httpbin.org
  rules:
  - matches: 
    - path:
        type: Exact
        value: /get
    backendRefs:
    - name: httpbin-service-e2e-test
      port: 80
`
		var grpcrouteSpec = `
apiVersion: gateway.networking.k8s.io/v1
kind: GRPCRoute
metadata:
  name: %s
spec:
  parentRefs:
  - name: %s
  rules:
  - backendRefs:
    - name: grpc-infra-backend-v1
      port: 8080
`
		var tcpRouteSpec = `
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: TCPRoute
metadata:
  name: %s
spec:
  parentRefs:
  - name: %s
    sectionName: tcp
  rules:
  - backendRefs:
    - name: httpbin-service-e2e-test
      port: 80
`
		var udpRoute = `
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: UDPRoute
metadata:
  name: %s
spec:
  parentRefs:
  - name: %s
    sectionName: udp
  rules:
  - backendRefs:
    - name: %s
      port: %d
`
		var (
			dnsName string
			dnsPort int32
		)

		BeforeEach(func() {
			dnsSvc := s.NewCoreDNSService()
			dnsName = dnsSvc.Name
			dnsPort = dnsSvc.Spec.Ports[0].Port

			By("deploy grpc backend")
			s.DeployGRPCBackend()

			By("create GatewayProxy")
			Expect(s.CreateResourceFromString(s.GetGatewayProxySpec())).NotTo(HaveOccurred(), "creating GatewayProxy")

			By("create GatewayClass")
			Expect(s.CreateResourceFromString(s.GetGatewayClassYaml())).NotTo(HaveOccurred(), "creating GatewayClass")

			s.RetryAssertion(func() string {
				gcyaml, _ := s.GetResourceYaml("GatewayClass", s.Namespace())
				return gcyaml
			}).Should(
				And(
					ContainSubstring(`status: "True"`),
					ContainSubstring("message: the gatewayclass has been accepted by the apisix-ingress-controller"),
				),
				"check GatewayClass condition",
			)

			By("create Gateway")
			gateway := fmt.Sprintf(gatewaySpec, s.Namespace(), s.Namespace())
			Expect(s.CreateResourceFromString(gateway)).NotTo(HaveOccurred(), "creating Gateway")

			s.RetryAssertion(func() string {
				gcyaml, _ := s.GetResourceYaml("Gateway", s.Namespace())
				return gcyaml
			}).Should(
				And(
					ContainSubstring(`status: "True"`),
					ContainSubstring("message: the gateway has been accepted by the apisix-ingress-controller"),
				),
				"check Gateway condition status",
			)
		})
		getStatusLintener := func(listenerName string) (*gatewayv1.ListenerStatus, error) {
			var gateway gatewayv1.Gateway
			if err := s.GetKubeClient().Get(context.Background(), k8stypes.NamespacedName{
				Name:      s.Namespace(),
				Namespace: s.Namespace(),
			}, &gateway); err != nil {
				return nil, err
			}
			for _, listener := range gateway.Status.Listeners {
				if string(listener.Name) == listenerName {
					return &listener, nil
				}
			}
			return nil, fmt.Errorf("listener %s not found", listenerName)
		}
		expectListenerAttachedRoutes := func(listenerName string, accectedRoutes int) {
			s.RetryAssertion(func() error {
				listener, err := getStatusLintener(listenerName)
				if err != nil {
					return err
				}
				if listener.AttachedRoutes != int32(accectedRoutes) {
					return fmt.Errorf("expected listener %s attached routes to be %d, got %d", listener.Name, accectedRoutes, listener.AttachedRoutes)
				}
				return nil
			}).ShouldNot(HaveOccurred(), "check listener attached routes")
		}
		It("check attachedRoutes and supportedkinds to gateway status", func() {
			By("check HTTPRoute/GRPCRoute attachedRoutes and supportedkinds to gateway status")
			s.ResourceApplied("HTTPRoute", "httpbin", fmt.Sprintf(httprouteSpec, "httpbin", s.Namespace()), 1)
			expectListenerAttachedRoutes("http", 1)
			for i := 0; i < 10; i++ {
				name := fmt.Sprintf("httpbin-%d", i)
				s.ResourceApplied("HTTPRoute", name, fmt.Sprintf(httprouteSpec, name, s.Namespace()), 1)
			}
			expectListenerAttachedRoutes("http", 11)
			for i := 0; i < 10; i++ {
				name := fmt.Sprintf("grcproute-%d", i)
				s.ResourceApplied("GRPCRoute", name, fmt.Sprintf(grpcrouteSpec, name, s.Namespace()), 1)
			}
			expectListenerAttachedRoutes("http", 21)

			listenner, err := getStatusLintener("http")
			Expect(err).NotTo(HaveOccurred(), "get http listener status")
			Expect(listenner.SupportedKinds).To(HaveLen(2), "http listener supported kinds length")
			Expect(listenner.SupportedKinds).To(ContainElements(
				gatewayv1.RouteGroupKind{
					Group: ptr.To(gatewayv1.Group(gatewayv1.GroupName)),
					Kind:  "HTTPRoute",
				},
				gatewayv1.RouteGroupKind{
					Group: ptr.To(gatewayv1.Group(gatewayv1.GroupName)),
					Kind:  "GRPCRoute",
				},
			), "http listener supported kinds content")

			By("check TCPRoute attachedRoutes and supportedkinds to gateway status")
			name := "tcp-route"
			s.ResourceApplied("TCPRoute", name, fmt.Sprintf(tcpRouteSpec, name, s.Namespace()), 1)
			expectListenerAttachedRoutes("tcp", 1)
			for i := 0; i < 10; i++ {
				name := fmt.Sprintf("tcp-route-%d", i)
				s.ResourceApplied("TCPRoute", name, fmt.Sprintf(tcpRouteSpec, name, s.Namespace()), 1)
			}
			expectListenerAttachedRoutes("tcp", 11)
			getListener, err := getStatusLintener("tcp")
			Expect(err).NotTo(HaveOccurred(), "get tcp listener status")
			Expect(getListener.SupportedKinds).To(HaveLen(1), "tcp listener supported kinds length")
			Expect(string(getListener.SupportedKinds[0].Kind)).To(Equal("TCPRoute"), "tcp listener supported kind content")

			By("check UDPRoute attachedRoutes and supportedkinds to gateway status")
			name = "udp-route"
			s.ResourceApplied("UDPRoute", name, fmt.Sprintf(udpRoute, name, s.Namespace(), dnsName, dnsPort), 1)
			expectListenerAttachedRoutes("udp", 1)
			for i := 0; i < 10; i++ {
				name := fmt.Sprintf("udp-route-%d", i)
				s.ResourceApplied("UDPRoute", name, fmt.Sprintf(udpRoute, name, s.Namespace(), dnsName, dnsPort), 1)
			}
			expectListenerAttachedRoutes("udp", 11)
			getListener, err = getStatusLintener("udp")
			Expect(err).NotTo(HaveOccurred(), "get udp listener status")
			Expect(getListener.SupportedKinds).To(HaveLen(1), "udp listener supported kinds length")
			Expect(string(getListener.SupportedKinds[0].Kind)).To(Equal("UDPRoute"), "udp listener supported kind content")
		})
	})
})
